因為要操作 GPU 前一定要經過 CPU 才能操控,所以需要有個流程,把從設定要處理的資料格式、到等待 GPU 處理完畢回傳結果,最後把結果呈現這些全部串在一起。
CPU
│
│ 1. 更新遊戲邏輯
│ 2. 計算物件狀態
│ 3. 準備 Draw Call / Command List
▼
Command Queue
│
▼
GPU
│
│ 4. 執行繪圖指令
│ 5. Vertex Shader
│ 6. Rasterization
│ 7. Pixel Shader
│ 8. 寫入 Render Target
▼
Back Buffer
│
│ Present
▼
螢幕顯示
...大概這種概念
我們常說的 FPS,其實也就是這整體在一秒可以被完成多少次數。
而在這中間要開始做效能優化的話,就會先去確定他是 CPU Bound 還是 GPU Bound,然後更進一步的去檢查其中的問題。
雖然稍微有點超出這章的內容範圍,不過可以先簡單提個例子。
假設我今天要畫一個非常大片的顏色格子、上面會有紅色、藍色、綠色…等等高達上萬的顏色要繪製在場景上讓玩家走跳。

大概可以想像成這樣
然後如果真的照字面上面的意思,去創建 10000 個 Mesh ,讓每個 Mesh 都顯示不同顏色的話,流程可能會變成
// CPU 要先一一傳遞資料進 GPU 去創建 Mesh
for (int i = 0; i < 10000; ++i)
{
SetVertexBuffer(mesh[i]);
SetIndexBuffer(mesh[i]);
SetColor(mesh[i].color);
DrawIndexedInstanced(...);
}
//從 CPU 到 GPU 的流程
CPU
↓
Draw Mesh 0
Draw Mesh 1
Draw Mesh 2
Draw Mesh 3
...
Draw Mesh 9999
↓
Command Queue
↓
GPU
而當然,創建 1 個 Mesh 的問題不只如此,一個 Mesh 會帶著很多資料,除了一般的 Vertex 跟 Index,還有 CIIndex、Color、Normal 、TexCoord、Lightmap…等等一大堆的資料。
就算可能最小 1 個 Mesh 他生這些預設值只會佔 32 KB,但 10000 個 Mesh 就會 320000KB。就算 CPU GPU 在怎麼強也無法真的很好處理這樣數量集的資料。
而另一種做法就是只生一個 Mesh,然後用 StructuredBuffer 傳資料進 Shader,讓他直接在 GPU 裡繪製顏色。
// 先用 struct 定義好要傳的顏色
struct PlaneData
{
float3 position;
float4 color;
};
// StructuredBuffer 的概念之後會提到,現在理解成也是一個可以傳資料的格式就好
StructuredBuffer<PlaneData> planeData : register(t0);
//寫一個類似這樣的程式一次傳完
DrawIndexedInstanced(
6, // 每個 Quad 6 個 index
10000, // 10000 instances
0,
0,
0
);
// 在 GPU 繪製時直接在 Shader 設定其資料與之後的顏色
PlaneData data = planeData[instanceID];
// 最後的流程大概是這種感覺
CPU
設定 Mesh
設定 StructuredBuffer
DrawIndexedInstanced(... 10000 instances ...)
↓
GPU
Instance 0 → data[0]
Instance 1 → data[1]
Instance 2 → data[2]
...
Instance 9999 → data[9999]
這樣就能解決 Mesh 造成過度佔據記憶體的問題。
最後顯示其設定相關的程式設定與實際程式
typedef struct D3D12_RESOURCE_BARRIER {
D3D12_RESOURCE_BARRIER_TYPE Type; // 指定此 Resource Barrier 的類型,例如 Transition、Aliasing 或 UAV
D3D12_RESOURCE_BARRIER_FLAGS Flags; // 設定此 Resource Barrier 的額外控制旗標
union {
D3D12_RESOURCE_TRANSITION_BARRIER Transition; // 描述 Resource 從一種 State 切換到另一種 State 的資訊
D3D12_RESOURCE_ALIASING_BARRIER Aliasing; // 描述共用同一塊 GPU 記憶體的不同 Resource 之間的切換關係
D3D12_RESOURCE_UAV_BARRIER UAV; // 確保 UAV 的前一次讀寫完成後,後續 UAV 存取才繼續執行
};
} D3D12_RESOURCE_BARRIER;
graphics_engine.h
#pragma once
#include <array>
#include "my_engine.h"
#include <d3d12.h>
#include <dxgi1_6.h>
#include <memory>
#include <directxtk12//GraphicsMemory.h>
#include "render_context.h"
class GraphicsEngine
{
public:
GraphicsEngine() = default;
~GraphicsEngine() noexcept;
GraphicsEngine(const GraphicsEngine&) = delete;
GraphicsEngine& operator=(const GraphicsEngine&) = delete;
GraphicsEngine(GraphicsEngine&&) = delete;
GraphicsEngine& operator=(GraphicsEngine&&) = delete;
bool init(HWND hwnd, UINT frameBufferWidth, UINT frameBufferHeight);
void resize(UINT frameBufferWidth, UINT frameBufferHeight);
[[nodiscard]]
ComPtr<IDXGIFactory7> createDXGIFactory();
void beginRender();
void endRender();
void waitDraw();
[[nodiscard]]
ID3D12Device14* getD3DDevice() const noexcept
{
return m_d3dDevice.Get();
}
[[nodiscard]]
UINT getBackBufferIndex() const noexcept
{
return m_frameIndex;
}
[[nodiscard]]
UINT getFrameBufferWidth() const noexcept
{
return m_frameBufferWidth;
}
[[nodiscard]]
UINT GetFrameBufferHeight() const noexcept
{
return m_frameBufferHeight;
}
[[nodiscard]]
ID3D12Resource* getDepthStencilBuffer() const noexcept
{
return m_depthStencilBuffer.Get();
}
[[nodiscard]]
D3D12_CPU_DESCRIPTOR_HANDLE getCurrentRenderTargetView() const;
[[nodiscard]]
D3D12_CPU_DESCRIPTOR_HANDLE getDepthStencilView() const;
enum : UINT
{
FRAME_BUFFER_COUNT = 2
};
private:
bool createD3DDevice(IDXGIFactory7* dxgiFactory);
bool createSwapChain(
HWND hwnd,
UINT frameBufferWidth,
UINT frameBufferHeight,
IDXGIFactory7* dxgiFactory
);
bool createSynchronizationWithGPUObject();
void createBackBufferViews();
void createDepthStencilBuffer();
ComPtr<IDXGISwapChain4> m_swapChain;
std::array<ComPtr<ID3D12Resource>, FRAME_BUFFER_COUNT> m_renderTargets;
ComPtr<ID3D12Fence> m_fence;
int m_currentBackBufferIndex = 0;
UINT m_rtvDescriptorSize = 0;
UINT m_frameIndex = 0;
HANDLE m_fenceEvent = nullptr;
UINT64 m_fenceValue = 0;
UINT m_frameBufferWidth = 0;
UINT m_frameBufferHeight = 0;
RenderContext m_renderContext;
std::unique_ptr<DirectX::GraphicsMemory> m_directXTKGfxMemory;
};
extern GraphicsEngine* g_graphicsEngine;
graphics_engine.cpp
//...其他程式
void GraphicsEngine::beginRender()
{
// 每一幀開始時,向 Swap Chain 查詢目前可寫入的 Back Buffer
m_frameIndex = m_swapChain->GetCurrentBackBufferIndex();
// 重設 Command Allocator,回收上一輪命令記錄使用的記憶體
if (FAILED(m_commandAllocator->Reset()))
throw std::runtime_error("GraphicsEngine: Failed to reset the command allocator.");
if (FAILED(m_commandList->Reset(m_commandAllocator.Get(), nullptr)))
throw std::runtime_error("GraphicsEngine: Failed to reset the command list.");
//使用 D3D12_RESOURCE_BARRIER 去做資源狀態轉換
D3D12_RESOURCE_BARRIER barrier{};
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barrier.Transition.pResource = m_renderTargets[m_frameIndex].Get();
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT;
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET;
barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
m_commandList->ResourceBarrier(1, &barrier);
// Viewport 將結果映射到畫面像素範圍
const D3D12_VIEWPORT viewport{
0.0f,
0.0f,
static_cast<float>(m_frameBufferWidth),
static_cast<float>(m_frameBufferHeight),
0.0f,
1.0f};
// Scissor Rect 限制實際上顯示的範圍
const D3D12_RECT scissorRect{
0,
0,
static_cast<LONG>(m_frameBufferWidth),
static_cast<LONG>(m_frameBufferHeight)};
m_commandList->RSSetViewports(1, &viewport);
m_commandList->RSSetScissorRects(1, &scissorRect);
// 取得目前 Back Buffer 的 RTV,以及深度緩衝區的 DSV
const D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = getCurrentRenderTargetView();
const D3D12_CPU_DESCRIPTOR_HANDLE dsvHandle = getDepthStencilView();
// 將目前 RTV 與 DSV 綁定到 Output Merger
m_commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, &dsvHandle);
constexpr float clearColor[] = {0.08f, 0.10f, 0.14f, 1.0f};
m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
m_commandList->ClearDepthStencilView(
dsvHandle,
D3D12_CLEAR_FLAG_DEPTH,
1.0f,
0,
0,
nullptr);
}
void GraphicsEngine::endRender()
{
//繪圖完畢後,把 Back Buffer 從 RENDER_TARGET 狀態轉回 PRESENT
D3D12_RESOURCE_BARRIER barrier{};
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barrier.Transition.pResource = m_renderTargets[m_frameIndex].Get();
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT;
barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
m_commandList->ResourceBarrier(1, &barrier);
if (FAILED(m_commandList->Close()))
throw std::runtime_error("GraphicsEngine: Failed to close the command list.");
ID3D12CommandList* commandLists[] = {m_commandList.Get()};
m_commandQueue->ExecuteCommandLists(1, commandLists);
if (FAILED(m_swapChain->Present(1, 0)))
throw std::runtime_error("GraphicsEngine: Failed to present the frame.");
// 等待 GPU 完成本幀工作
waitDraw();
}
D3D12_RESOURCE_BARRIER (d3d12.h) - Win32 apps
ChatGPT 生範例圖